﻿using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;

namespace LFSRecord2.Model.Sound
{
    public delegate void BufferFillEventHandler(IntPtr data, int size);
    public delegate void BufferPopulateEventHandler(int size);

    internal sealed class WaveOutBuffer : IDisposable
    {
        public WaveOutBuffer NextBuffer;

        /// <summary>
        /// Thread manager
        /// </summary>
        private AutoResetEvent _playEvent = new AutoResetEvent(false);
        /// <summary>
        /// A handle to our wave out device
        /// </summary>
        private IntPtr _waveOut;

        /// <summary>
        /// The header that we send to the out device describing and containing the wave data to output
        /// </summary>
        private NativeMethods.WaveHdr _header;
        /// <summary>
        /// The actual buffer
        /// </summary>
        private byte[] _headerData;

        /// <summary>
        /// A handle to the header object
        /// </summary>
        private GCHandle _headerHandle;
        /// <summary>
        /// A handle to the _headerData contained in the header object
        /// </summary>
        private GCHandle _headerDataHandle;

        private bool _playing;

        internal static void WaveOutProc(IntPtr hdrvr, int uMsg, int dwUser, ref NativeMethods.WaveHdr wavhdr, int dwParam2)
        {
            if (uMsg == NativeMethods.MM_WOM_DONE)
            {
                try
                {
                    GCHandle h = (GCHandle)wavhdr.dwUser;
                    WaveOutBuffer buf = (WaveOutBuffer)h.Target;
                    buf.OnCompleted();
                }
                catch
                {
                }
            }
        }

        public WaveOutBuffer(IntPtr waveOutHandle, int size)
        {
            _waveOut = waveOutHandle;

            _headerHandle = GCHandle.Alloc(_header, GCHandleType.Pinned);
            _header.dwUser = (IntPtr)GCHandle.Alloc(this);
            _headerData = new byte[size];
            _headerDataHandle = GCHandle.Alloc(_headerData, GCHandleType.Pinned);
            _header.lpData = _headerDataHandle.AddrOfPinnedObject();
            _header.dwBufferLength = size;
            WaveHelper.Try(NativeMethods.waveOutPrepareHeader(_waveOut, ref _header, Marshal.SizeOf(_header)));
        }
        ~WaveOutBuffer()
        {
            Dispose();
        }
        public void Dispose()
        {
            lock (this)
            {
                if (_header.lpData != IntPtr.Zero)
                {
                    try
                    {
                        WaveHelper.Try(NativeMethods.waveOutUnprepareHeader(_waveOut, ref _header, Marshal.SizeOf(_header)));
                    }
                    catch
                    {
                        //Debug.WriteLine("Buffer Dispose exception : " + ex.Message);
                    }
                    _headerHandle.Free();
                    _header.lpData = IntPtr.Zero;
                }
                _playEvent.Close();
                if (_headerDataHandle.IsAllocated)
                    _headerDataHandle.Free();
            }
            GC.SuppressFinalize(this);
        }

        public int Size
        {
            get { return _header.dwBufferLength; }
        }

        public IntPtr Data
        {
            get { return _header.lpData; }
        }

        /// <summary>
        /// Set thread to nonsignaled, so threads block (when WaitOne is called) and then write data to the device
        /// </summary>
        /// <returns></returns>
        public bool Play()
        {
            lock (this)
            {
                _playEvent.Reset();
                _playing = (NativeMethods.waveOutWrite(_waveOut, ref _header, Marshal.SizeOf(_header)) == NativeMethods.MMSYSERR_NOERROR);
                return _playing;
            }
        }

        /// <summary>
        /// Lets the thread wait for a signal
        /// </summary>
        public void WaitFor()
        {
            if (_playing)
            {
                _playing = _playEvent.WaitOne();
            }
            else
            {
                Thread.Sleep(0);
            }
        }

        /// <summary>
        /// Signal the thread, so we can continue
        /// </summary>
        public void OnCompleted()
        {
            _playEvent.Set();
            _playing = false;
        }
    }

    public sealed class WaveOutDevice : IDisposable
    {
        private IntPtr _waveOut;
        private WaveOutBuffer _buffers; // linked list
        private WaveOutBuffer _currentBuffer;
        private Thread _thread;
        private BufferFillEventHandler _fillProc;
        private BufferPopulateEventHandler _propProc;
        private bool _finished;
        private byte _zero;

        private NativeMethods.WaveDelegate _bufferProc = new NativeMethods.WaveDelegate(WaveOutBuffer.WaveOutProc);

        public WaveOutDevice(int device, WaveFormatEx format, int bufSize, int bufCount, BufferPopulateEventHandler propProc, BufferFillEventHandler fillProc)
        {
            if (bufSize % 4 > 0)
                throw new Exception("WaveOut buffer size is not a multiple of 4.");

            WaveHelper.Try(NativeMethods.waveOutOpen(out _waveOut, device, format, _bufferProc, IntPtr.Zero, NativeMethods.CALLBACK_FUNCTION));

            _zero = format.wBitsPerSample == 8 ? (byte)128 : (byte)0;
            _propProc = propProc;
            _fillProc = fillProc;
            AllocateBuffers(bufSize, bufCount);

            _thread = new Thread(new ThreadStart(ThreadProc));
            _thread.Start();
        }
        ~WaveOutDevice()
        {
            Dispose();
        }
        public void Dispose()
        {
            //Debug.WriteLine("audio device disposed (A)");
            if (_thread != null)
            {
                if (!_finished)
                {
                    _finished = true;
                    return;
                }

                try
                {
                    if (_waveOut != IntPtr.Zero)
                        NativeMethods.waveOutReset(_waveOut);
                    //_thread.Join();
                    _propProc = null;
                    _fillProc = null;
                    FreeBuffers();
                    if (_waveOut != IntPtr.Zero)
                        NativeMethods.waveOutClose(_waveOut);
                }
                catch (Exception ex)
                {
                    Debug.WriteLine("WaveOutDevice dispose exception : " + ex.Message);
                }
                finally
                {
                    //Debug.WriteLine("audio device disposed (H)");
                    _thread = null;
                    _propProc = null;
                    _fillProc = null;
                    _waveOut = IntPtr.Zero;
                }
            }
            GC.SuppressFinalize(this);
        }

        private void ThreadProc()
        {
            while (!_finished)
            {
                AdvanceBuffer();
                if (_fillProc != null && !_finished)
                {
                    // Get data from our external data supplier and read it into our playback buffer
                    _fillProc(_currentBuffer.Data, _currentBuffer.Size);
                }
                else
                {
                    // zero out buffer - silence
                    byte v = _zero;
                    byte[] b = new byte[_currentBuffer.Size];
                    for (int i = 0; i < b.Length; i++)
                        b[i] = v;
                    Marshal.Copy(b, 0, _currentBuffer.Data, b.Length);
                }

                _currentBuffer.Play();

                if (_propProc != null && !_finished)
                {
                    // Read data into the mixer's buffer (read-ahead)
                    _propProc(_currentBuffer.Size);
                }

            }
            WaitForAllBuffers();
            Dispose();
        }

        private void AllocateBuffers(int bufSize, int bufCount)
        {
            FreeBuffers();
            if (bufCount > 0)
            {
                _buffers = new WaveOutBuffer(_waveOut, bufSize);
                WaveOutBuffer Prev = _buffers;
                try
                {
                    for (int i = 1; i < bufCount; i++)
                    {
                        WaveOutBuffer Buf = new WaveOutBuffer(_waveOut, bufSize);
                        Prev.NextBuffer = Buf;
                        Prev = Buf;
                    }
                }
                finally
                {
                    Prev.NextBuffer = _buffers;
                }
            }
        }

        private void FreeBuffers()
        {
            _currentBuffer = null;
            if (_buffers != null)
            {
                WaveOutBuffer First = _buffers;
                _buffers = null;

                WaveOutBuffer Current = First;
                do
                {
                    WaveOutBuffer Next = Current.NextBuffer;
                    Current.Dispose();
                    Current = Next;
                } while (Current != First);
            }
        }

        /// <summary>
        /// Go to next buffer in the chain and wait for a thread signal before returning
        /// </summary>
        private void AdvanceBuffer()
        {
            _currentBuffer = (_currentBuffer == null) ? _buffers : _currentBuffer.NextBuffer;
            _currentBuffer.WaitFor();
        }

        private void WaitForAllBuffers()
        {
            WaveOutBuffer Buf = _buffers;
            while (Buf.NextBuffer != _buffers)
            {
                Buf.WaitFor();
                Buf = Buf.NextBuffer;
            }
            //Debug.WriteLine("done waiting for all buffers");
        }
    }
}
